-module(katzenreis_scan).

-export([scan_file/1, scan/1, string_to_value/1]).

-include("katzenreis.hrl").

-import(katzenreis_utils, [file_open_error/2, unquote_string/1]).


%% @doc Scans a template file identified by `Path' and returns template nodes.
%% @spec (string()) -> [template_node()]
%% @since #since

-spec scan_file(string()) -> [template_node()].

scan_file(Path) -> 
  case file:read_file(Path) of
    {ok, Binary} -> scan(binary_to_list(Binary));
    {error, Reason} -> file_open_error(Path, Reason)
  end.

scan([]) -> [];
scan(Source) -> scan(Source, 1, text, []).

scan([], Line, _State, Nodes) ->
  End = [{'$end', Line}|Nodes],
  lists:reverse(End);
scan(Source, Line, text, Nodes) ->
  case scan_text(Source) of
    {text, Text, Rest, ScanLines} ->
      NextLine = Line + ScanLines,
      Node = {text, Line, Text},
      scan(Rest, NextLine, text, [Node|Nodes]);
    {begin_var, Rest} ->
      scan(Rest, Line, var, Nodes);
    {begin_tag, Rest} ->
      scan(Rest, Line, tag, Nodes)
  end;
scan(Source, Line, var, Nodes) ->
  case scan_primary(Source, Line) of
    {match, Expr, Source1, Line1} ->
      scan_filters(Source1, Line1, Expr, Line1, [], Nodes);
    nomatch -> throw({parse_error, Line, "expected variable"})
  end;
scan(Source, Line, tag, Nodes) ->
  {Nodes1, Source1, Line1} = scan_tag(Source, Line, Nodes),
  scan(Source1, Line1, text, Nodes1).

scan_primary(Source, Line) ->
  case scan_ident(Source, Line) of
    {match, Name, Source1, Line1} ->
      {match, list_to_atom(Name), Source1, Line1};
    nomatch ->
      case scan_string(Source, Line) of
        {match, _, _, _} = M -> M;
        nomatch ->
          case scan_float(Source, Line) of
            {match, _, _, _} = M -> M;
            nomatch ->
              case scan_integer(Source, Line) of
                {match, _, _, _} = M -> M;
                nomatch -> throw({parse_error, Line,
                  "expected variable, string, integer or float"})
              end
          end
      end
  end.

scan_ident(Source, Line) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  match(Source1, Line1, ?IDENT_PATTERN).

scan_string(Source, Line) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  case match(Source1, Line1, ?STR_PATTERN) of
    {match, Match, Source2, Line2} ->
      {match, string:substr(Match, 2, length(Match)-2), Source2, Line2};
    nomatch -> nomatch
  end.

scan_integer(Source, Line) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  case match(Source1, Line1, ?INT_PATTERN) of
    {match, Match, Source2, Line2} ->
      {match, list_to_integer(Match), Source2, Line2};
    nomatch -> nomatch
  end.

scan_float(Source, Line) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  case match(Source1, Line1, ?FLOAT_PATTERN) of
    {match, Match, Source2, Line2} ->
      {match, list_to_float(Match), Source2, Line2};
    nomatch -> nomatch
  end.

scan_filters(Source, Line, Value, ValueLine, Filters, Nodes) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  case Source1 of
    "}}"++Source2 ->
      Node = {var, ValueLine, {Value, lists:reverse(Filters)}},
      scan(Source2, Line1, text, [Node|Nodes]);
    "|"++Source2 ->
      {FilterName, Arg, Source3, Line2} = scan_filter(Source2, Line1),
      Filter = {Line2, FilterName, Arg},
      scan_filters(Source3, Line2, Value, ValueLine, [Filter|Filters], Nodes);
    _ -> throw({parse_error, Line, "expected `|' or `}}'"})
  end.

scan_filter(Source, Line) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  case scan_ident(Source1, Line1) of
    {match, Name, Source2, Line2} ->
      NameAtom = list_to_atom(Name),
      {Source3, Line3} = ignore_spaces(Source2, Line2),
      case Source3 of
        ":"++Source4 -> scan_filter_arg(Source4, Line3, NameAtom);
        _ -> {NameAtom, nil, Source3, Line3}
      end;
    nomatch -> throw({parse_error, Line1, "expected filter name"})
  end.

scan_filter_arg(Source, Line, Name) ->
  case scan_ident(Source, Line) of
    {match, Match, Source1, Line1} -> {Name, Match, Source1, Line1};
    nomatch ->
      case scan_string(Source, Line) of
        {match, Match, Source1, Line1} -> {Name, Match, Source1, Line1};
        nomatch ->
          case scan_integer(Source, Line) of
            {match, Match, Source1, Line1} -> {Name, Match, Source1, Line1};
            nomatch ->
              case scan_float(Source, Line) of
                {match, Match, Source1, Line1} -> {Name, Match, Source1, Line1};
                nomatch ->
                  throw({parse_error, Line,
                    "expected value or keyword as a tag argument"})
              end
          end
      end
  end.

scan_tag(Source, Line, Nodes) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  case match(Source1, Line1, ?IDENT_PATTERN) of
    {match, Name, Source2, Line2} ->
      {ArgNodes, Source3, Line3} = scan_tag_args(Source2, Line2, []),
      Node = {tag, Line3, {list_to_atom(Name), ArgNodes}},
      {[Node|Nodes], Source3, Line3};
    nomatch -> throw({parse_error, Line1, "expected tag name"})
  end.

scan_tag_args(Source, Line, Nodes) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  case Source1 of
    "%}"++Source2 -> {lists:reverse(Nodes), Source2, Line1};
    _ ->
      {Arg, Source2, Line2} = scan_tag_arg(Source1, Line1),
      Node = {Line2, Arg},
      scan_tag_args(Source2, Line2, [Node|Nodes])
  end.

scan_tag_arg(Source, Line) ->
  case match_node(Source, Line, ?ARG_PATTERN) of
    {match, Match, Source1, Line1} -> {Match, Source1, Line1};
    nomatch -> throw({parse_error, Line,
      "expected value or keyword as a tag argument"})
  end.

ignore_spaces(Source, Line) -> ignore_spaces1(Source, Line).

ignore_spaces1("\r\n"++Source, Line) -> ignore_spaces1(Source, Line+1);
ignore_spaces1("\n"++Source, Line) -> ignore_spaces1(Source, Line+1);
ignore_spaces1("\r"++Source, Line) -> ignore_spaces1(Source, Line+1);
ignore_spaces1("\f"++Source, Line) -> ignore_spaces1(Source, Line);
ignore_spaces1("\v"++Source, Line) -> ignore_spaces1(Source, Line);
ignore_spaces1("\t"++Source, Line) -> ignore_spaces1(Source, Line);
ignore_spaces1(" "++Source, Line) -> ignore_spaces1(Source, Line);
ignore_spaces1(Source, Line) -> {Source, Line}.

match_node(Source, Line, Pattern) ->
  {Source1, Line1} = ignore_spaces(Source, Line),
  match(Source1, Line1, Pattern).

match(Source, Line, Pattern) ->
  case re:run(Source, Pattern) of
    {match, [{Start, End}|_]} ->
      Text = string:sub_string(Source, Start+1, End),
      Rest = string:substr(Source, End + 1),
      {match, Text, Rest, count_nls(Text, Line)};
    nomatch -> nomatch
  end.

count_nls([], Line) -> Line;
count_nls("\r\n"++Source, Line) -> count_nls(Source, Line+1);
count_nls("\n"++Source, Line) -> count_nls(Source, Line+1);
count_nls("\r"++Source, Line) -> count_nls(Source, Line+1);
count_nls([_Char|Source], Line) -> count_nls(Source, Line).

scan_text([]) -> {text, "", [], 0};
scan_text("{%"++Source) -> {begin_tag, Source};
scan_text("{{"++Source) -> {begin_var, Source};
scan_text(Source) -> scan_text1(Source, "", 0).

scan_text1([], Buf, Lines) ->
  {text, lists:reverse(Buf), [], Lines};
scan_text1("{{"++_Rest = Source, Buf, Lines) ->
  {text, lists:reverse(Buf), Source, Lines};
scan_text1("{%"++_Rest = Source, Buf, Lines) ->
  {text, lists:reverse(Buf), Source, Lines};
scan_text1("\r\n"++Source, Buf, Lines) ->
  scan_text1(Source, "\r\n" ++ Buf, Lines+1);
scan_text1("\n"++Source, Buf, Lines) ->
  scan_text1(Source, "\n" ++ Buf, Lines+1);
scan_text1("\r"++Source, Buf, Lines) ->
  scan_text1(Source, "\n" ++ Buf, Lines+1);
scan_text1([Char|Source], Buf, Lines) -> scan_text1(Source, [Char|Buf], Lines).

string_to_value(Str) ->
  case re:run(Str, ?STR_PATTERN) of
    {match, _} -> {ok, unquote_string(Str)};
    nomatch ->
      case re:run(Str, ?FLOAT_PATTERN) of
        {match, _} -> {ok, list_to_float(Str)};
        nomatch ->
          case re:run(Str, ?INT_PATTERN) of
            {match, _} -> {ok, list_to_integer(Str)};
            nomatch ->
              case re:run(Str, ?IDENT_PATTERN) of
                {match, _} -> {var, list_to_atom(Str)};
                nomatch ->
                  {error, "unknown format -- " ++ Str}
              end
          end
      end
  end.

